/*
 * Copyright 2011 Katsunori Koyanagi
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package sweezy.core.reflect
{
	import flash.system.ApplicationDomain;
	import flash.system.System;
	import sweezy.core.utils.ClassUtils;

	internal class DefaultTypeFactory implements ITypeFactory
	{

		private var _cache:Object;

		public function DefaultTypeFactory()
		{
			_cache = {};
		}

		public function clearCache():void
		{
			var name:String;
			for (name in _cache)
			{
				delete _cache[name];
			}
			ClassUtils.clearDescribeCache();
		}

		public function createType(className:String, applicationDomain:ApplicationDomain):IType
		{
			if (className in _cache)
			{
				return _cache[className];
			}

			var type:TypeImpl = new TypeImpl(className, applicationDomain);
			_cache[className] = type;
			return type;
		}

		public function get loadedTypes():Array
		{
			var result:Array = [];
			var index:uint = 0;

			for (var key:String in _cache)
			{
				result[index++] = _cache[key];
			}

			return result;
		}
	}
}

import flash.system.ApplicationDomain;
import flash.utils.describeType;
import sweezy.core.reflect.IConstructor;
import sweezy.core.reflect.IMember;
import sweezy.core.reflect.IMetadata;
import sweezy.core.reflect.IMetadataArgument;
import sweezy.core.reflect.IMetadataEntry;
import sweezy.core.reflect.IMethod;
import sweezy.core.reflect.IPackageInfo;
import sweezy.core.reflect.IParameter;
import sweezy.core.reflect.IProperty;
import sweezy.core.reflect.IType;
import sweezy.core.reflect.Reflection;
import sweezy.core.utils.ClassUtils;
import sweezy.core.utils.StringUtils;

function forName0(className:String, applicationDomain:ApplicationDomain):TypeImpl
{
	return Reflection.forName(className, applicationDomain) as TypeImpl;
}

class TypeImpl implements IType
{

	internal var _className:String;

	internal var _shortName:String;

	private var _applicationDomain:ApplicationDomain;

	private var _constructor:ConstructorImpl;

	private var _constructorInitialized:Boolean;

	private var _extendsClasses:Array;

	private var _implementsInterfaces:Array;

	private var _isInterface:Boolean;

	private var _metadata:MetadataImpl;

	private var _methodInitialized:Boolean;

	private var _methods:Array;

	private var _namedMethodMap:Object;

	private var _namedPropertyMap:Object;

	private var _namedStaticMethodMap:Object;

	private var _namedStaticPropertyMap:Object;

	private var _packageInfo:PackageInfoImpl;

	private var _properties:Array;

	private var _propertiesInitialized:Boolean;

	private var _rawClass:Class;

	private var _superClass:TypeImpl;

	private var _typeInitialized:Boolean;

	public function TypeImpl(className:String, applicationDomain:ApplicationDomain)
	{
		_applicationDomain = applicationDomain;
		_className = className;
	}

	public function get applicationDomain():ApplicationDomain
	{
		return _applicationDomain;
	}

	public function get className():String
	{
		return _className;
	}

	public function get constructor():IConstructor
	{
		if (!_typeInitialized)
		{
			initType();
		}
		if (!_constructorInitialized)
		{
			initConstructor();
		}

		return _constructor;
	}

	public function get extendsClasses():Array
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _extendsClasses.slice(0);
	}

	public function getMethod(name:String, isStatic:Boolean, uri:String = null):IMethod
	{
		if (!name)
		{
			return null;
		}

		if (!_typeInitialized)
		{
			initType();
		}
		if (!_methodInitialized)
		{
			initMethods();
		}

		if (uri !== null)
		{
			name = new QName(new Namespace(uri), name).toString();
		}

		return isStatic ? _namedStaticMethodMap[name] : _namedMethodMap[name];
	}

	public function getMethods(filter:Function):Array
	{
		var result:Array = methods;
		if (filter === null)
		{
			return result;
		}

		return result.filter(filter);
	}

	public function getProperties(filter:Function):Array
	{
		var result:Array = properties;
		if (filter === null)
		{
			return result;
		}

		return result.filter(filter);
	}

	public function getProperty(name:String, isStatic:Boolean, uri:String = null):IProperty
	{
		if (!name)
		{
			return null;
		}

		if (!_typeInitialized)
		{
			initType();
		}
		if (!_propertiesInitialized)
		{
			initProperties();
		}

		if (uri !== null)
		{
			name = new QName(new Namespace(uri), name).toString();
		}

		return isStatic ? _namedStaticPropertyMap[name] : _namedPropertyMap[name];
	}

	public function get hasMetadata():Boolean
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _metadata.numEntries > 0;
	}

	public function get implementsInterfaces():Array
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _implementsInterfaces.slice(0);
	}

	public function isAssignableFrom(type:IType):Boolean
	{
		if (!_typeInitialized)
		{
			initType();
		}

		if (_rawClass === Object || _rawClass === Wildcard)
		{
			return true;
		}
		if (_rawClass === Void)
		{
			return false;
		}

		var t:TypeImpl;
		var className:String = type.className;
		for each (t in type.implementsInterfaces)
		{
			if (t._className === _className)
			{
				return true;
			}
		}

		if (type.isInterface)
		{
			return false;
		}

		for each (t in type.extendsClasses)
		{
			if (t._className === _className)
			{
				return true;
			}
		}

		return false;
	}

	public function get isInterface():Boolean
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _isInterface;
	}

	public function get metadata():IMetadata
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _metadata;
	}

	public function get methods():Array
	{
		if (!_typeInitialized)
		{
			initType();
		}
		if (!_methodInitialized)
		{
			initMethods();
		}

		return _methods.slice(0);
	}

	public function newInstance(args:Array = null):*
	{
		if (!_typeInitialized)
		{
			initType();
		}

		if (_rawClass === Void)
		{
			return undefined;
		}
		else if (_rawClass === Wildcard)
		{
			return {};
		}

		return _constructor.newInstance(args);
	}

	public function get packageInfo():IPackageInfo
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _packageInfo;
	}

	public function get properties():Array
	{
		if (!_typeInitialized)
		{
			initType();
		}
		if (!_propertiesInitialized)
		{
			initProperties();
		}

		return _properties.slice(0);
	}

	public function get rawClass():Class
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _rawClass;
	}

	public function get shortName():String
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _shortName;
	}

	public function get superClass():IType
	{
		if (!_typeInitialized)
		{
			initType();
		}

		return _superClass;
	}

	public function toString():String
	{
		if (!_typeInitialized)
		{
			initType();
		}

		var result:String;
		var type:TypeImpl;
		var delim:String;

		if (_isInterface)
		{
			result = "public interface ";
			result += _className;

			if (_implementsInterfaces.length > 0)
			{
				result += " extends ";
				delim = "";
				for each (type in _implementsInterfaces)
				{
					result += delim;
					result += type._className;
					delim = ", ";
				}
			}
		}
		else
		{
			result = "public class ";
			result += _className;

			if (_superClass !== null)
			{
				result += " extends ";
				result += _superClass._className;
			}

			if (_implementsInterfaces.length > 0)
			{
				result += " implements ";
				delim = "";
				for each (type in _implementsInterfaces)
				{
					result += delim;
					result += type._className;
					delim = ", ";
				}
			}
		}

		result += "{}";
		return result;
	}

	private function getRawClass():Class
	{
		var fqcn:String = _className;
		if (fqcn === "*")
		{
			return Wildcard;
		}
		else if (fqcn === "void")
		{
			return Void;
		}

		if (fqcn.indexOf("__AS3__.vec::Vector") > -1 && fqcn.indexOf("*") > -1)
		{
			fqcn = fqcn.replace("*", "Object");
		}

		return ClassUtils.forName(fqcn, _applicationDomain);
	}

	private function initConstructor():void
	{
		_constructorInitialized = true;

		var nodes:XMLList = ClassUtils.describe(_rawClass).factory.constructor;
		_constructor = new ConstructorImpl(this, nodes[0]);
	}

	private function initMethods():void
	{
		_methodInitialized = true;
		_methods = [];
		_namedMethodMap = {};
		_namedStaticMethodMap = {};

		var xml:XML = ClassUtils.describe(_rawClass);
		for each (var node:XML in xml..method)
		{
			var method:IMethod = new MethodImpl(this, node);
			var name:String = method.name;
			var uri:String = method.uri;
			if (uri !== null)
			{
				name = new QName(new Namespace(uri), name).toString();
			}

			if (method.isStatic)
			{
				_namedStaticMethodMap[name] = method;
			}
			else
			{
				_namedMethodMap[name] = method;
			}
			_methods[_methods.length] = method;
		}
	}

	private function initProperties():void
	{
		_propertiesInitialized = true;
		_properties = [];
		_namedPropertyMap = {};
		_namedStaticPropertyMap = {};

		var xml:XML = ClassUtils.describe(_rawClass);

		initProperties0(xml..constant, 0);
		initProperties0(xml..variable, 1);
		initProperties0(xml..accessor, 2);
	}

	private function initProperties0(members:XMLList, memberType:uint):void
	{
		for each (var node:XML in members)
		{
			var property:PropertyImpl = new PropertyImpl(this, node, memberType);
			var name:String = property._name;
			if (property._uri !== null)
			{
				name = new QName(new Namespace(property._uri), name).toString();
			}

			if (property._isStatic)
			{
				_namedStaticPropertyMap[name] = property;
			}
			else
			{
				_namedPropertyMap[name] = property;
			}
			_properties[_properties.length] = property;
		}
	}

	private function initType():void
	{
		_typeInitialized = true;
		_rawClass = getRawClass();

		var xml:XML = ClassUtils.describe(_rawClass);
		var fqn:String = _className;
		var idx:int = fqn.indexOf("::");
		if (idx === -1)
		{
			_shortName = fqn;
			_packageInfo = PackageInfoImpl.ROOT;
		}
		else
		{
			_shortName = fqn.substring(idx + 2);
			_packageInfo = PackageInfoImpl.getPackageInfo(fqn.substring(0, idx));
		}

		_metadata = MetadataImpl.createMetaData(xml.factory.metadata);

		var node:XML;
		var ad:ApplicationDomain = applicationDomain;

		_extendsClasses = [];
		_implementsInterfaces = [];

		var type:TypeImpl;
		var index:uint = 0;
		for each (node in xml.factory.extendsClass)
		{
			type = forName0(node.@type, ad);
			_extendsClasses[index++] = type;

			if (_superClass === null)
			{
				_superClass = type;
			}
		}
		_isInterface = (_rawClass === Object) ? false : (_extendsClasses.length === 0);

		index = 0;
		for each (node in xml.factory.implementsInterface)
		{
			_implementsInterfaces[index++] = forName0(node.@type, ad);
		}
	}
}

class MetadataImpl implements IMetadata
{

	public static const EMPTY:MetadataImpl = new MetadataImpl(null);

	public static function createMetaData(metadataList:XMLList):MetadataImpl
	{
		return metadataList.length() === 0 ? EMPTY : new MetadataImpl(metadataList);
	}

	private var _entries:Array;

	private var _namedEntryMap:Object;

	public function MetadataImpl(metadataList:XMLList)
	{
		_entries = [];
		_namedEntryMap = {};

		for each (var metadata:XML in metadataList)
		{
			var entry:MetadataEntryImpl = new MetadataEntryImpl(metadata);
			_entries[_entries.length] = entry;

			var entries:Array;
			if (entry._name in _namedEntryMap)
			{
				entries = _namedEntryMap[entry._name];
			}
			else
			{
				entries = _namedEntryMap[entry._name] = [];
			}
			entries[entries.length] = entry;
		}
	}

	public function get entries():Array
	{
		return _entries.slice(0);
	}

	public function getEntriesByName(name:String):Array
	{
		if (!(name in _namedEntryMap))
		{
			return [];
		}

		return _namedEntryMap[name].slice(0);
	}

	public function getEntryByName(name:String):IMetadataEntry
	{
		if (!(name in _namedEntryMap))
		{
			return null;
		}

		return _namedEntryMap[name][0];
	}

	public function hasEntry(name:String):Boolean
	{
		return name in _namedEntryMap;
	}

	public function get numEntries():uint
	{
		return _entries.length;
	}

	public function toString():String
	{
		return _entries.join("\n");
	}
}

class MetadataEntryImpl implements IMetadataEntry
{

	internal var _name:String;

	private var _arguments:Array;

	private var _namedArgumentMap:Object;

	public function MetadataEntryImpl(metadata:XML)
	{
		_arguments = [];
		_namedArgumentMap = {};
		_name = metadata.@name;

		var index:uint = 0;
		for each (var arg:XML in metadata.arg)
		{
			var argument:MetadataArgumentImpl = new MetadataArgumentImpl(arg);
			_arguments[index++] = argument;

			var args:Array;
			if (argument._key in _namedArgumentMap)
			{
				args = _namedArgumentMap[argument._key];
			}
			else
			{
				args = _namedArgumentMap[argument._key] = [];
			}

			args[args.length] = argument;
		}
	}

	public function get arguments():Array
	{
		return _arguments.slice(0);
	}

	public function getArgumentByKey(key:String):IMetadataArgument
	{
		if (!(key in _namedArgumentMap))
		{
			return null;
		}

		return _namedArgumentMap[key][0];
	}

	public function getArgumentsByKey(key:String):Array
	{
		if (!(key in _namedArgumentMap))
		{
			return null;
		}

		return _namedArgumentMap[key].slice(0);
	}

	public function hasArgument(key:String):Boolean
	{
		return key in _namedArgumentMap;
	}

	public function get name():String
	{
		return _name;
	}

	public function toString():String
	{
		var result:String = "[";
		result += _name;

		if (_arguments.length > 0)
		{
			result += "(";
			result += _arguments.join(", ");
			result += ")";
		}

		result += "]";
		return result;
	}
}

class MetadataArgumentImpl implements IMetadataArgument
{

	internal var _key:String;

	private var _value:String;

	public function MetadataArgumentImpl(arg:XML)
	{
		_key = arg.@key;
		_value = arg.@value;
	}

	public function get key():String
	{
		return _key;
	}

	public function toString():String
	{
		var result:String = "";

		if (_key)
		{
			result += key;
			result += " = ";
		}

		result += StringUtils.escapeESString(_value);
		return result;
	}

	public function get value():String
	{
		return _value;
	}
}

class MemberImpl implements IMember
{

	internal var _name:String;

	private var _declaringType:IType;

	private var _metadata:IMetadata;

	public function MemberImpl(declaringType:IType, name:String, metadata:IMetadata)
	{
		_declaringType = declaringType;
		_name = name;
		_metadata = metadata;
	}

	public function get declaringType():IType
	{
		return _declaringType;
	}

	public function get hasMetadata():Boolean
	{
		return _metadata.numEntries > 0;
	}

	public function get metadata():IMetadata
	{
		return _metadata;
	}

	public function get name():String
	{
		return _name;
	}
}

class ParameterImpl implements IParameter
{

	public static function createParameters(member:XML, applicationDomain:ApplicationDomain):Array
	{
		if (member === null)
		{
			return [];
		}

		var parameterList:XMLList = member.parameter;
		var result:Array = [];

		for (var i:int = 0, len:int = parameterList.length(); i < len; i++)
		{
			var param:XML = parameterList[i];
			var index:uint = uint(param.@index);
			var type:TypeImpl = forName0(param.@type, applicationDomain);
			var optional:Boolean = String(param.@optional) === "true";

			result[i] = new ParameterImpl(index, type, optional);
		}

		return result;
	}

	private var _index:uint;

	private var _optional:Boolean;

	private var _type:TypeImpl;

	public function ParameterImpl(index:uint, type:TypeImpl, optional:Boolean)
	{
		_index = index;
		_type = type;
		_optional = optional;
	}

	public function get index():uint
	{
		return _index;
	}

	public function get optional():Boolean
	{
		return _optional;
	}

	public function toString():String
	{
		var result:String = "arg";
		result += (_index - 1);
		result += ":";
		result += _type._className;

		if (_optional)
		{
			result += " = undefined";
		}

		return result;
	}

	public function get type():IType
	{
		return _type;
	}
}

class MethodImpl extends MemberImpl implements IMethod
{

	private var _declaredType:TypeImpl;

	private var _isStatic:Boolean;

	private var _parameters:Array;

	private var _returnType:TypeImpl;

	private var _uri:String;

	public function MethodImpl(declaringType:IType, member:XML)
	{
		super(declaringType, member.@name, MetadataImpl.createMetaData(member.metadata));

		var ad:ApplicationDomain = declaringType.applicationDomain;
		_parameters = ParameterImpl.createParameters(member, ad);
		_returnType = forName0(member.@returnType, ad);
		_declaredType = forName0(member.@declaredBy, ad);
		_isStatic = String(member.parent().name()) === "type";

		if ("@uri" in member)
		{
			_uri = String(member.@uri);
		}
	}

	public function get declaredType():IType
	{
		return _declaredType;
	}

	public function invoke(instance:*, args:Array = null):*
	{
		var fn:Function, thiz:*;
		if (_isStatic)
		{
			fn = declaringType.rawClass[name];
			thiz = null;
		}
		else
		{
			fn = instance[name];
			thiz = instance;
		}

		return fn.apply(thiz, args);
	}

	public function get isStatic():Boolean
	{
		return _isStatic;
	}

	public function get parameters():Array
	{
		return _parameters.slice(0);
	}

	public function get returnType():IType
	{
		return _returnType;
	}

	public function toString():String
	{
		var result:String;

		if (uri)
		{
			result = uri;
		}
		else
		{
			result = "public";
		}
		result += " ";

		if (_isStatic)
		{
			result += "static ";
		}

		result += "function ";
		result += name;
		result += "(";
		result += _parameters.join(", ");
		result += "):";
		result += _returnType.className;
		result += ";";

		return result;
	}

	public function get uri():String
	{
		return _uri;
	}
}

class ConstructorImpl extends MemberImpl implements IConstructor
{

	private var _parameters:Array;

	public function ConstructorImpl(declaringType:TypeImpl, member:XML)
	{
		super(declaringType, declaringType._shortName, MetadataImpl.EMPTY);
		_parameters = ParameterImpl.createParameters(member, declaringType.applicationDomain);
	}

	public function newInstance(args:Array = null):*
	{
		return ClassUtils.newInstance(declaringType.rawClass, args);
	}

	public function get parameters():Array
	{
		return _parameters.slice(0);
	}

	public function toString():String
	{
		return "public function " + name + "(" + parameters.join(", ") + ");";
	}
}


class PropertyImpl extends MemberImpl implements IProperty
{

	private static const CONST:int = 1;

	private static const PROPERTY:int = 0;

	private static const VARIABLE:int = 2;

	internal var _isStatic:Boolean;

	internal var _uri:String;

	private var _memberType:int;

	private var _readable:Boolean;

	private var _type:TypeImpl;

	private var _writable:Boolean;

	public function PropertyImpl(declaringType:TypeImpl, member:XML, memberType:uint)
	{
		super(declaringType, member.@name, MetadataImpl.createMetaData(member.metadata));

		switch (memberType)
		{
			case 0:
				_readable = true;
				_writable = false;
				_memberType = CONST;
				break;
			case 1:
				_readable = true;
				_writable = true;
				_memberType = VARIABLE;
				break;
			default:
				var access:String = member.@access;
				_readable = (access === "readwrite") || (access === "readonly");
				_writable = (access === "readwrite") || (access === "writeonly");
				_memberType = PROPERTY;
				break;
		}

		_isStatic = String(member.parent().name()) === "type";
		_type = forName0(member.@type, declaringType.applicationDomain);

		if ("@uri" in member)
		{
			_uri = String(member.@uri);
		}
	}

	public function getValue(instance:*):*
	{
		if (_isStatic)
		{
			return declaringType.rawClass[name];
		}

		return instance[name];
	}

	public function get isConst():Boolean
	{
		return _memberType === CONST;
	}

	public function get isProperty():Boolean
	{
		return _memberType === PROPERTY;
	}

	public function get isStatic():Boolean
	{
		return _isStatic;
	}

	public function get isVariable():Boolean
	{
		return _memberType === VARIABLE;
	}

	public function get readable():Boolean
	{
		return _readable;
	}

	public function setValue(instance:*, value:*):void
	{
		if (_isStatic)
		{
			declaringType.rawClass[name] = value;
			return;
		}

		instance[name] = value;
	}

	public function toString():String
	{
		var result:String = "";

		switch (_memberType)
		{
			case CONST:
				result += prefix;
				result += "const ";
				result += name;
				result += ":";
				result += _type.className;
				result += ";";
				break;

			case VARIABLE:
				result += prefix;
				result += "var ";
				result += name;
				result += ":";
				result += _type.className;
				result += ";";
				break;

			default:
				if (_readable)
				{
					result += prefix;
					result += "function get ";
					result += name;
					result += "():";
					result += _type.className;
					result += ";"
				}
				if (_writable)
				{
					result += prefix;
					result += "function set ";
					result += name;
					result += "(value:";
					result += _type.className;
					result += "):void;"
				}
				break;
		}

		return result;
	}

	public function get type():IType
	{
		return _type;
	}

	public function get uri():String
	{
		return _uri;
	}

	public function get writable():Boolean
	{
		return _writable;
	}

	private function get prefix():String
	{
		var result:String;

		if (uri)
		{
			result = uri;
		}
		else
		{
			result = "public";
		}
		result += " ";

		if (_isStatic)
		{
			result += "static ";
		}

		return result;
	}
}


class PackageInfoImpl implements IPackageInfo
{

	public static const ROOT:PackageInfoImpl = new PackageInfoImpl();

	public static function getPackageInfo(packageName:String):PackageInfoImpl
	{
		if (!packageName)
		{
			return ROOT;
		}

		var names:Array = StringUtils.trim(packageName).split(/\./g);
		var info:PackageInfoImpl = ROOT;
		var newInfo:PackageInfoImpl;
		var fullName:String = "";

		for (var i:int = 0, len:int = names.length; i < len; i++)
		{
			var name:String = names[i];
			fullName += name;

			if (name in info._children)
			{
				info = info._children[name];
			}
			else
			{
				newInfo = new PackageInfoImpl();
				newInfo._fullName = fullName;
				newInfo._name = name;
				info = info._children[name] = newInfo;
			}

			fullName += ".";
		}

		return info;
	}

	private var _children:Object;

	private var _fullName:String;

	private var _name:String;

	private var _parent:PackageInfoImpl;

	public function PackageInfoImpl()
	{
		if (ROOT === null)
		{
			_name = "";
			_fullName = "";
		}

		_children = {};
	}

	public function get fullName():String
	{
		return _fullName;
	}

	public function get isRoot():Boolean
	{
		return _parent === null;
	}

	public function get name():String
	{
		return _name;
	}

	public function get parent():IPackageInfo
	{
		return _parent;
	}
}

class Wildcard
{
}

class Void
{
}